package net.masker.bbtweet;

import java.io.IOException;
import java.io.InputStream;
import java.io.ByteArrayInputStream;
import java.io.InputStreamReader;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.NoSuchElementException;
import java.util.Timer;
import java.util.TimerTask;

import javax.microedition.io.Connector;
import javax.microedition.io.HttpConnection;
import javax.microedition.io.StreamConnection;

import net.rim.blackberry.api.browser.URLEncodedPostData;
import net.rim.blackberry.api.mail.Address;
import net.rim.blackberry.api.mail.Folder;
import net.rim.blackberry.api.mail.FolderNotFoundException;
import net.rim.blackberry.api.mail.Message;
import net.rim.blackberry.api.mail.Session;
import net.rim.blackberry.api.mail.Store;
import net.rim.blackberry.api.mail.event.MessageEvent;
import net.rim.blackberry.api.mail.event.MessageListener;
import net.rim.device.api.io.http.HttpProtocolConstants;
import net.rim.device.api.notification.NotificationsManager;
import net.rim.device.api.ui.component.Dialog;
import net.rim.device.api.xml.parsers.*;
import org.w3c.dom.*;

class ConnectionManager extends Thread implements MessageListener
{
	private boolean starting = true;
	private static Properties properties = Properties.fetch();
    private static String friendsTimelineURL = "http://twitter.com/statuses/friends_timeline.xml";
    private static String repliesURL = "http://twitter.com/statuses/replies.xml";
    private static String directMessagesURL = "http://twitter.com/direct_messages.xml";
    private static String shortenerURL = "http://is.gd/api.php";
    private long[] lowID = {0, 0, 0, 0};
    private Hashtable delivered = new Hashtable(400);
    private Timer friendsTimer = new Timer();
    private Timer repliesTimer = new Timer();
    private Timer directTimer = new Timer();
    
    
    
    /**
	 * Parses XML stream, returning root element
	 * @param stream
	 * @return
	 */
    private final Element parseXML(InputStream stream)
	{
		
		try {
			/* 8.10 trying to fix mysterious SAXParse errors */
			int ch;
			String responseString = "";
        	while ((ch = stream.read())!= -1) {
        		responseString += (char)ch;
        	}
        	responseString.trim();
        	InputStream xml = new ByteArrayInputStream(responseString.getBytes());
        	/* end */
        	
		//Parse input stream, get root element & normalize, returning root element
		DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        DocumentBuilder builder = factory.newDocumentBuilder();
        //Document document = builder.parse(stream);
        Document document = builder.parse(xml);
        
        Element rootElement = document.getDocumentElement();
        rootElement.normalize();
        
        return rootElement;
        
		}
		catch (Exception e)
		{
			properties.setBbtweetStatusMessage("Retrieval: Error parsing response");
        	properties.save();
			System.out.println(e.toString());
			return null;
		}
	}
    
    public void run()
    {
    	// Wait until last instance exits
    	while (properties.isConnectionManagerStarted())
    	{
    		try
    		{
				sleep(500);
			}
    		catch (InterruptedException e)
    		{
				// Couldn't sleep
			}
    		properties = Properties.fetch();
    	}
    	
    	properties.setConnectionManagerStarted(true);
    	properties.save();
    	
    	if (properties.isBbtweetEnabled())
    	{
    		if (properties.getFriendsInterval() > 0)
    			getFriendsUpdates();
    		if (properties.getRepliesInterval() > 0)
    			getReplies();
    		if (properties.getDirectInterval() > 0)
    			getDirectMessages();
    	}
    	
    	// Start-up phase is complete
    	starting = false;
    }

	// Message listener for changed messages
	public void changed(MessageEvent msgEvt)
	{
		// If a bbtweet message has been opened, stop notification
		try
		{
			NotificationsManager.cancelImmediateEvent(net.masker.bbtweet.bbtweet.NOTIFICATIONS_ID_1, 99,
					"bbtweet", "MessageOpened");
		}
		catch (Exception e)
		{
			// Exception can be raised if notification is not active, ignore
		}
	}
	
	private final void getUpdates(String url)
	{
		int indexID = getIndexID(url);
		// Open stream and poll data
        try
        {
			// Create post data
			long lastID = properties.getLastID(indexID);
        	URLEncodedPostData post = new URLEncodedPostData("", false);
			/*switch (lastID)
			{
				case -1: // initial run, pull lastID and store, don't deliver messages
					post.append("count", "1");
					break;
				case 0: // reset for testing, pull and deliver last 20 messages
					break;
				default: // pull all messages since lastID
					post.append("count", "200");
					post.append("since_id", Integer.toString(lastID));
			}*/
        	if (lastID == -1)
        		post.append("count", "1");
        	else if (lastID != 0) {
        		post.append("count", "200");
				post.append("since_id", Long.toString(lastID));
        	}
			// Create connection
        	StreamConnection s = null;
			s = (StreamConnection)Connector.open(url + "?" + post.toString(), Connector.READ_WRITE, true);
            HttpConnection httpConn = (HttpConnection)s;
            httpConn.setRequestProperty(HttpProtocolConstants.HEADER_AUTHORIZATION,"Basic "
            		+ properties.getAuthenticationHash());

            // Connect and check status
            int status = httpConn.getResponseCode();
            
            //Nick W. 8.7.K9 handle redirects: Twitter has been using these to combat DoS attacks the past few days
            String newURI = httpConn.getHeaderField("Location");
            if (status == HttpConnection.HTTP_TEMP_REDIRECT || status == HttpConnection.HTTP_MOVED_TEMP || status == HttpConnection.HTTP_MOVED_PERM) {
            	if (newURI != null) {
            			//great, we have the new URL: go get it
            		s = null;
            		httpConn = null;
        			s = (StreamConnection)Connector.open(newURI + "?" + post.toString(), Connector.READ_WRITE, true);
                    httpConn = (HttpConnection)s;
                    httpConn.setRequestProperty(HttpProtocolConstants.HEADER_AUTHORIZATION, "Basic " + properties.getAuthenticationHash());
                    status = httpConn.getResponseCode();
            	} else {
            			//give it a second to populate
            		long begin_wait = System.currentTimeMillis();
            		while (System.currentTimeMillis() - begin_wait < 1000 || status != HttpConnection.HTTP_OK) {
            			try {
        					sleep(100);
            			} catch (InterruptedException e) {
        					// Couldn't sleep
            			}
            			status = httpConn.getResponseCode();
            		}
            	}
            }

            if (status == HttpConnection.HTTP_OK) {
            	// Log success
            	properties.setBbtweetStatusMessage("Retrieval: HTTP_OK 200");
            	properties.save();
            	
            	// Create element, null connection
            	
            	// for testing
            	//StringBuffer out = new StringBuffer();
            	//InputStream in = httpConn.openInputStream();
            	//byte[] b = new byte[4096];
            	//for (int n; (n = in.read(b)) != -1;)
            	//{
            	//	out.append(new String(b, 0, n));
            	//}
            	//String test = out.toString();
            	// end for testing
            	
            	Element root = parseXML(httpConn.openInputStream());
            	httpConn = null;
            	s = null;
            	
				if (root != null) 
				{
					if (indexID < 2)// status update
						parseStatusMessages(root, indexID, lastID);
					else // direct message
						parseDirectMessages(root, indexID, lastID);
				}
            	
            } else {
            	// Log failure
            	properties.setBbtweetStatusMessage("Retrieval: " + httpConn.getResponseMessage());
            	properties.save();
            }
        }
        catch (IOException e) 
        {
        	// Log IO Exception
        	properties.setBbtweetStatusMessage("Retrieval: " + e.toString());
        	properties.save();
        }
	}
	
	private final void getFriendsUpdates()
	{
		// retrieve updates
		getUpdates(friendsTimelineURL);
		
		// reset timer
		properties = Properties.fetch();
		friendsTimer = new Timer();
		friendsTimer.schedule(new TimerTask()
		{
			public void run()
			{
				getFriendsUpdates();
			}
		}, (properties.getFriendsInterval() * 60000));
	}
	
	private final void getReplies()
	{
		// retrieve updates
		getUpdates(repliesURL);
		
		// reset timer
		properties = Properties.fetch();
		repliesTimer = new Timer();
		repliesTimer.schedule(new TimerTask()
		{
			public void run()
			{
				getReplies();
			}
		}, (properties.getRepliesInterval() * 60000));
	}
	
	private final void getDirectMessages()
	{
		// retrieve updates
		getUpdates(directMessagesURL);
		
		// reset timer
		properties = Properties.fetch();
		directTimer = new Timer();
		directTimer.schedule(new TimerTask()
		{
			public void run()
			{
				getDirectMessages();
			}
		}, (properties.getDirectInterval() * 60000));
	}
	
	private final int getIndexID(String url)
	{
		if (url.equals(friendsTimelineURL))
			return 0;
		if (url.equals(repliesURL))
			return 1;
		if (url.equals(directMessagesURL))
			return 3;
		
		return -1;
	}
	
	private final void parseStatusMessages(Element root, int indexID, long lastID)
	{
		long newID = lastID;
		// Loop through statuses and deliver messages
		for (int i = root.getElementsByTagName("status").getLength() - 1; i >=0; --i)
		{
			long thisID = Long.parseLong(root.getElementsByTagName("id").item(2 * i).getFirstChild().getNodeValue());
			
			if (thisID > lastID) // new messages
			{
				if (thisID > newID) // set newID to this ID if higher
					newID = thisID;
				
				// discard messages from user and messages on first run
				String screenName = root.getElementsByTagName("screen_name").item(i).getFirstChild().getNodeValue();
				if ((screenName.compareTo(properties.getUsername()) == 0) || (lastID == -1))
					continue;
				
				// discard messages already delivered
				if (delivered.containsKey(new Long(thisID)))
					continue;
				
				// Open store and attempt to select "no service book" inbox
				Store store = Session.getDefaultInstance().getStore();
				Folder inbox;
				try
				{
					inbox = store.getFolder(2);
				}
				catch (FolderNotFoundException e)
				{
					// If that doesn't work, just grab the first inbox
					Folder[] folders = store.list(Folder.INBOX);
					inbox = folders[0];
				}
				
				// Create the message
				Message msg = new Message(inbox);
				try
				{
					// Create the message object
					Address from = new Address("http://bbtweet.googlecode.com/", "@" + screenName);
					msg.setFrom(from);
					msg.setContent(root.getElementsByTagName("text").item(i).getFirstChild().getNodeValue());
					msg.setInbound(true);
					
					// Add message to inbox and set status to received
					inbox.appendMessage(msg);
					msg.setStatus(Message.Status.RX_RECEIVED, 0);
					
					// Add MessageListener to message
					msg.addMessageListener(this);
					
					// Trigger immediate notification
					NotificationsManager.triggerImmediateEvent(bbtweet.NOTIFICATIONS_ID_1, 0, this, null);
					
					// Add id to delivered list
					delivered.put(new Long(thisID), new Integer(1));
				}
				catch (Exception e)
				{
					// Log exception
					properties.setBbtweetStatusMessage("Retrieval: " + e.toString());
		        	properties.save();
				}
			}
		}
		
		// Clean up
		root = null;
		
		// Log and update last ID
		properties.setBbtweetStatusMessage("Complete");
		properties.setLastID(indexID, newID);
		properties.save();
		
		// Update lowID
		lowID[indexID] = newID;
		lowID[2] = (lowID[0] < lowID[1] ? lowID[0] : lowID[1]);
		if (lowID[3] < lowID[2])
			lowID[2] = lowID[3];
		
		// cleanup delivered list
		Hashtable newDelivered = new Hashtable(400);
		Enumeration oldDelivered = delivered.keys();
		for (;;)
		{
			try
			{
				Long thisElement = (Long) oldDelivered.nextElement();
				
				if (thisElement.longValue() >= lowID[2])
					newDelivered.put(thisElement, new Integer(1));
			}
			catch (NoSuchElementException e)
			{
				delivered = newDelivered;
				newDelivered = null;
				oldDelivered = null;
				break;
			}
		}
	}
	
	private final void parseDirectMessages(Element root, int indexID, long lastID)
	{
		long newID = lastID;
		
		// Loop through DMs and deliver messages
		for (int i = root.getElementsByTagName("direct_message").getLength() - 1; i >=0; --i)
		{
			long thisID = Long.parseLong(root.getElementsByTagName("id").item(2 * i).getFirstChild().getNodeValue());
			
			if (thisID > lastID) // new messages
			{
				if (thisID > newID) // set newID to this ID if higher
					newID = thisID;
				
				// discard messages from user and messages on first run
				String screenName = root.getElementsByTagName("sender_screen_name").item(i).getFirstChild().getNodeValue();
				// if ((screenName.compareTo(properties.getUsername()) == 0) || (lastID == -1))
				// 	continue;
				
				// discard messages already delivered
				if (delivered.containsKey(new Long(thisID)))
					continue;
				
				// Open store and attempt to select "no service book" inbox
				Store store = Session.getDefaultInstance().getStore();
				Folder inbox;
				try
				{
					inbox = store.getFolder(2);
				}
				catch (FolderNotFoundException e)
				{
					// If that doesn't work, just grab the first inbox
					Folder[] folders = store.list(Folder.INBOX);
					inbox = folders[0];
				}
				
				// Create the message
				Message msg = new Message(inbox);
				try
				{
					// Create the message object
					Address from = new Address("http://bbtweet.googlecode.com/", "d @" + screenName);
					msg.setFrom(from);
					msg.setContent(root.getElementsByTagName("text").item(i).getFirstChild().getNodeValue());
					msg.setInbound(true);
					
					// Add message to inbox and set status to received
					inbox.appendMessage(msg);
					msg.setStatus(Message.Status.RX_RECEIVED, 0);
					
					// Add MessageListener to message
					msg.addMessageListener(this);
					
					// Trigger immediate notification
					NotificationsManager.triggerImmediateEvent(bbtweet.NOTIFICATIONS_ID_1, 0, this, null);
					
					// Add id to delivered list
					delivered.put(new Long(thisID), new Integer(1));
				}
				catch (Exception e)
				{
					// Log exception
					properties.setBbtweetStatusMessage("Retrieval: " + e.toString());
		        	properties.save();
				}
			}
		}
		
		// Clean up
		root = null;
		
		// Log and update last ID
		properties.setBbtweetStatusMessage("Complete");
		properties.setLastID(indexID, newID);
		properties.save();
		
		// Update lowID
		lowID[indexID] = newID;
		lowID[2] = (lowID[0] < lowID[1] ? lowID[0] : lowID[1]);
		if (lowID[3] < lowID[2])
			lowID[2] = lowID[3];
		
		// cleanup delivered list
		Hashtable newDelivered = new Hashtable(400);
		Enumeration oldDelivered = delivered.keys();
		for (;;)
		{
			try
			{
//				Integer thisElement = (Integer) oldDelivered.nextElement();
//				
//				if (thisElement.intValue() >= lowID[2])
//					newDelivered.put(thisElement, new Integer(1));
				Long thisElement = (Long) oldDelivered.nextElement();
				
				if (thisElement.longValue() >= lowID[2])
					newDelivered.put(thisElement, new Integer(1));
			}
			catch (NoSuchElementException e)
			{
				delivered = newDelivered;
				newDelivered = null;
				oldDelivered = null;
				break;
			}
		}
	}
	
	public ConnectionManager killTimers()
	{	
		// If this thread is still in start-up phase, return immediately 
		// without doing anything
		if (starting)
			return this;
		
		ConnectionManager newConnectionManager = new ConnectionManager();
		newConnectionManager.start();
		
		// Cancel and null timers, run in thread so that this returns immediately 
    	new Thread(new Runnable()
    		{
    			public void run()
    			{
    				friendsTimer.cancel();
    				repliesTimer.cancel();
    		    	directTimer.cancel();
    		    	friendsTimer = null;
    		    	repliesTimer = null;
    		    	directTimer = null;
    		    	
    		    	// Hold until this, new ConnectionManager, & GUI are the only threads left running
    		    	while (activeCount() > 3)
    		    	{
    		    		try
    		    		{
							sleep(500);
						} 
    		    		catch (InterruptedException e) 
						{
							// couldn't sleep
						}
    		    	}
    		    	
    		    	// shutdown complete
    		    	properties = Properties.fetch();
    		    	properties.setConnectionManagerStarted(false);
    		    	properties.save();
    			}
    		}).start();

    		return newConnectionManager;
	}
	
	public final String shortenURL(String url)
	{
		InputStreamReader reader;
		char[] shortURL;
		
		try
        {
			// Create post data
			URLEncodedPostData post = new URLEncodedPostData("", false);
			post.append("longurl", url);
			
			// Create connection
        	StreamConnection s = null;
			s = (StreamConnection)Connector.open(shortenerURL + "?" + post.toString(), Connector.READ_WRITE, true);
            HttpConnection httpConn = (HttpConnection)s;

            // Connect and check status, bail if bad
            int status = httpConn.getResponseCode();
            if (status != HttpConnection.HTTP_OK)
            	throw new IOException("HTTP response code: " + status);
            
        	// Log success
        	properties.setBbtweetStatusMessage("Retrieval: HTTP_OK 200");
        	properties.save();
        	
        	// Get short URL
        	reader = new InputStreamReader(httpConn.openInputStream());
        	shortURL = new char[(int)httpConn.getLength()];
        	reader.read(shortURL);
        	httpConn = null;
        	s = null;
            return new String(shortURL);
            	
        }
        catch (IOException e) 
        {
        	// Log IO Exception
        	properties.setBbtweetStatusMessage("ShortenURL: " + e.toString());
        	properties.save();
        	return new String("");
        }
	}
}